Builder in JAVA
contents
롬복(Lombok) 라이브러리에서 개발자들이 사랑하는 애노테이션 중 하나인 @Builder 에 대해 알아보겠습니다.
이 애노테이션은 빌더 패턴(Builder Pattern) 을 자동으로 구현해 줍니다.
빌더 패턴은 복잡한 객체를 단계별로 생성할 수 있게 해주는 "생성 디자인 패턴"입니다. 롬복 없이 빌더를 직접 짜려면 지루한 보일러플레이트 코드(상용구)를 50줄 넘게 작성해야 하지만, @Builder를 쓰면 단 한 줄로 끝납니다.
작동 원리와 내부 구조, 그리고 흔히 겪는 실수들에 대한 분석입니다.
1. 문제점: "점층적 생성자 (Telescoping Constructor)"
필드가 5개인 User 클래스가 있다고 상상해 봅시다.
// 빌더 없이 생성할 때
User user = new User("John", "Doe", 30, "john@email.com", true);
- 문제: 읽기가 너무 어렵습니다.
30이 나이(age)인지 ID인지 헷갈립니다.true는 "활성 상태"를 뜻하는지 "관리자 여부"를 뜻하는지 알 수 없습니다. 만약 6번째 필드를 추가한다면, 프로젝트 전체에서new User(...)를 호출하는 모든 코드를 다 고쳐야 합니다.
빌더 사용 시:
User user = User.builder()
.firstName("John")
.lastName("Doe")
.age(30)
.email("john@email.com")
.active(true)
.build();
- 장점: 가독성이 뛰어나고, 유연하며, 파라미터의 순서에 구애받지 않습니다.
2. 사용 방법
클래스 위에 애노테이션만 붙이면 됩니다.
import lombok.Builder;
import lombok.ToString;
@Builder
@ToString
public class User {
private final String firstName;
private final String lastName;
private int age;
}
3. 내부 동작: 롬복이 생성하는 코드
이 코드를 컴파일하면, 롬복의 애노테이션 프로세서가 정적 내부 클래스(Static Inner Class) 를 생성해 줍니다. 대략 아래와 같은 모양입니다.
public class User {
private final String firstName;
private final String lastName;
private int age;
// 1. private 생성자 (외부에서 직접 생성 못 하고 빌더를 쓰도록 강제)
User(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
// 2. 빌더 인스턴스를 얻는 정적 메서드
public static UserBuilder builder() {
return new UserBuilder();
}
// 3. 정적 내부 클래스 (실제 빌더)
public static class UserBuilder {
private String firstName;
private String lastName;
private int age;
UserBuilder() {}
// 4. 체이닝이 가능한 세터(Setter) 메서드들
public UserBuilder firstName(String firstName) {
this.firstName = firstName;
return this; // 'this'를 반환하여 메서드 체이닝 가능
}
public UserBuilder lastName(String lastName) {
this.lastName = lastName;
return this;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
// 5. 최종 객체 생성 메서드
public User build() {
return new User(firstName, lastName, age);
}
}
}
4. 고급 기능 및 함정 (Gotchas)
A. @Builder.Default (조용한 살인마)
필드에 기본값을 넣어두더라도, @Builder는 기본적으로 이를 무시합니다.
시나리오:
@Builder
public class User {
private String name;
private boolean active = true; // 기본값을 true로 설정함
}
사용:
User u = User.builder().name("John").build();
System.out.println(u.isActive()); // false가 출력됩니다! 😱
- 이유: 빌더는 모든 인자를 받는 생성자(All-Args Constructor)를 사용하여 객체를 만듭니다. 이때 빌더 체인에서 값을 지정하지 않은 필드에는
0,null,false같은 기본 타입의 초기값이 들어갑니다. 즉, 우리가 적어둔= true는 덮어씌워져서 무시됩니다. - 해결책:
@Builder.Default를 붙여야 합니다.
@Builder.Default
private boolean active = true;
B. @Singular (컬렉션 처리)
List나 Map이 있는 경우, 보통은 리스트를 먼저 만들어서 통째로 넘겨줘야 합니다.
@Singular를 쓰면 롬복이 원소를 하나씩 추가하는 메서드를 만들어줍니다.
@Builder
public class Group {
@Singular
private List members;
}
// 사용법
Group g = Group.builder()
.member("John") // 주의: 필드명은 members(복수)지만 메서드는 member(단수)로 생성됨
.member("Jane")
.build();
C. toBuilder = true (객체 복사)
이미 존재하는 객체와 거의 똑같은데 필드 몇 개만 바꾼 복사본을 만들고 싶을 때 유용합니다(불변 객체에 특히 유용).
@Builder(toBuilder = true)
public class User { ... }
// 사용법
User user1 = User.builder().name("John").age(30).build();
// user1의 데이터를 그대로 가져오되, 나이만 31로 변경하여 user2 생성
User user2 = user1.toBuilder().age(31).build();
5. 중요 충돌: @Builder + @NoArgsConstructor
개발자들이 가장 많이 겪는 컴파일 에러입니다.
충돌 원인:
@Builder가 작동하려면 모든 인자가 있는 생성자(All-Args Constructor) 가 필요합니다. 생성자가 없으면 롬복이 알아서 만들어 씁니다.- JPA(Hibernate) 엔티티는 기본 생성자(No-Args Constructor) 가 필수입니다.
- 그래서
@NoArgsConstructor를 추가합니다. - 자바 규칙상 "개발자가 생성자를 하나라도 직접(여기선 애노테이션으로) 만들면, 기본 생성자는 사라진다"는 원칙 때문에, 1번에서 롬복이 암묵적으로 쓰던 All-Args 생성자가 사라집니다.
- 결과:
@Builder가 깨집니다.
해결책:
반드시 두 생성자를 모두 명시적으로 추가해야 합니다.
@Entity
@Builder
@NoArgsConstructor // JPA를 위해 필요
@AllArgsConstructor // 빌더를 위해 필요
public class User {
@Id
private Long id;
private String name;
}
6. 요약 테이블
| 기능 | 설명 |
|---|---|
| 기본 사용 | 클래스에 @Builder. ClassName.builder()...build() 사용 가능. |
| 기본값 | 필드 초기값은 무시됨. @Builder.Default를 붙여야 적용됨. |
| 컬렉션 | @Singular를 붙이면 리스트에 addOne() 하는 메서드가 생성됨. |
| 복사 | toBuilder = true를 쓰면 인스턴스를 복제하고 필드를 수정 가능. |
| 생성자 충돌 | JPA와 함께 쓸 때는 반드시 @AllArgsConstructor, @NoArgsConstructor 둘 다 추가. |
| 상속 | extends 상속 관계에서는 잘 작동 안 함. @SuperBuilder(실험적 기능) 사용 필요. |
references